﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using FileSharer.Properties;
using System.Diagnostics;
using System.IO;

namespace FileSharer
{
    public class ChangesetWatcher
    {
        private Thread m_WatcherThread = null;

        private TeamFoundationServer GetServer()
        {
            TeamFoundationServer server = TeamFoundationServerFactory.GetServer(Settings.Default.TeamFoundationServerUrl);
            return server;
        }

        private VersionControlServer GetVersionControl()
        {
            TeamFoundationServer server = this.GetServer();
            VersionControlServer versionControl = (VersionControlServer)server.GetService(typeof(VersionControlServer));
            return versionControl;
        }

        private int GetLatestChangesetId()
        {
            while (true)
            {
                try
                {
                    VersionControlServer versionControl = this.GetVersionControl();
                    int latestChangesetId = versionControl.GetLatestChangesetId();
                    return latestChangesetId;
                }
                catch (Exception ex)
                {
                    Trace.TraceError(
                        "An error occured getting the latest changeset ID. Will continue to retry:\n{0}",
                        ex
                        );
                }

                Thread.Sleep(Settings.Default.ChangesetPollInterval);
            }
        }

        private void Watcher()
        {
            int lastChangesetId = this.GetLatestChangesetId();

            while (true)
            {
                try
                {
                    this.WaitForChangeset(lastChangesetId);
                    List<ProcessingInstruction> instructions = this.GenerateProcessingInstructions(lastChangesetId);
                    int finalChangesetId = this.ExecuteProcessingInstructions(instructions);
                    lastChangesetId = finalChangesetId;
                }
                catch (Exception ex)
                {
                    Trace.TraceError(
                        "An unexpected error occured watching for shared file updates:\n{0}",
                        ex
                        );
                }
            }
        }

        private int ExecuteProcessingInstructions(List<ProcessingInstruction> instructions)
        {
            int finalChangesetId = instructions[0].Changeset.ChangesetId;

            foreach (ProcessingInstruction instruction in instructions)
            {
                finalChangesetId = this.ExecuteProcessingInstruction(instruction);
            }

            return finalChangesetId;
        }

        private Workspace GetWorkspace(VersionControlServer versionControl)
        {
            Workspace workspace = versionControl.TryGetWorkspace(Settings.Default.WorkspacePath);

            if (workspace == null)
            {
                WorkingFolder mapping = new WorkingFolder(
                    "$/",
                    Settings.Default.WorkspacePath
                    );

                workspace = versionControl.CreateWorkspace(Settings.Default.WorkspaceName);
                workspace.CreateMapping(mapping);
            }

            return workspace;
        }

        private int ExecuteProcessingInstruction(ProcessingInstruction instruction)
        {
            VersionControlServer versionControl = this.GetVersionControl();
            Workspace workspace = this.GetWorkspace(versionControl);

            List<string> serverItemsList = new List<string>();
            foreach (SharedFileReference reference in instruction.FileSet.SharedFileReferences)
            {
                serverItemsList.Add(reference.Path);
            }
            string[] serverItems = serverItemsList.ToArray();

            GetStatus status = workspace.Get(
                serverItems,
                VersionSpec.Latest,
                RecursionType.None,
                GetOptions.Overwrite
                );

            workspace.PendEdit(serverItems);

            string sourcePath = workspace.GetLocalItemForServerItem(instruction.Change.Item.ServerItem);
            foreach (string serverItem in serverItems)
            {
                string copyPath = workspace.GetLocalItemForServerItem(serverItem);
                if (sourcePath != copyPath)
                {
                    File.Copy(sourcePath, copyPath, true);
                }
            }

            string checkinComment = string.Format(
                "Checking-in files because '{0}' changed.",
                instruction.Change.Item.ServerItem
                );
            PendingChange[] changes = workspace.GetPendingChanges();
            int changesetId = workspace.CheckIn(changes, checkinComment);
            return changesetId;
       }

        private List<ProcessingInstruction> GenerateProcessingInstructions(int lastChangesetId)
        {
            VersionControlServer versionControl = this.GetVersionControl();
            int newChangesetId = this.GetLatestChangesetId();
            List<ProcessingInstruction> processingInstructionList = new List<ProcessingInstruction>();

            int changesetIdToFetch = lastChangesetId;
            do
            {
                changesetIdToFetch++;
                List<ProcessingInstruction> changesetProcessinInstructionList = this.GenerateProcessingInstructionsFromChangesetId(versionControl, changesetIdToFetch);
                processingInstructionList.AddRange(changesetProcessinInstructionList);

            } while (changesetIdToFetch < newChangesetId);

            List<ProcessingInstruction> instructionList = this.OrderChangesAndRemoveDuplicates(processingInstructionList);
            return instructionList;
        }

        private List<ProcessingInstruction> OrderChangesAndRemoveDuplicates(List<ProcessingInstruction> processingIntructionList)
        {
            processingIntructionList.Sort(
                delegate(ProcessingInstruction x, ProcessingInstruction y)
                    {
                        return y.Changeset.ChangesetId.CompareTo(x.Changeset.ChangesetId);
                    }
                );

            List<ProcessingInstruction> finalInstructionList = new List<ProcessingInstruction>();
            foreach (ProcessingInstruction instruction in processingIntructionList)
            {
                ProcessingInstruction existingInstruction = finalInstructionList.Find(
                    delegate(ProcessingInstruction item)
                    {
                        return item.FileSet.Name == instruction.FileSet.Name;
                    }
                    );
                if (existingInstruction == null) finalInstructionList.Add(instruction);
            }

            return finalInstructionList;
        }

        private FileSharerConfiguration m_Configuration = null;

        private SharedFileSet FindFileSet(string serverItemPath)
        {
            if (this.m_Configuration == null)
            {
                string processFileName = Process.GetCurrentProcess().MainModule.FileName;
                FileInfo processFileInfo = new FileInfo(processFileName);
                DirectoryInfo processDirectory = processFileInfo.Directory;
                string configurationFileName = Path.Combine(processDirectory.FullName, "FileSharer.xml");
                FileSharerConfiguration configuration = FileSharerConfiguration.Load(configurationFileName);
                this.m_Configuration = configuration;
            }

            SharedFileSet set = this.m_Configuration.Find(serverItemPath);
            return set;
        }

        private List<ProcessingInstruction> GenerateProcessingInstructionsFromChangesetId(VersionControlServer versionControl, int changesetIdToFetch)
        {
            Changeset changeset = versionControl.GetChangeset(changesetIdToFetch);
            List<ProcessingInstruction> changesetProcessingInstructionList = new List<ProcessingInstruction>();
            foreach (Change change in changeset.Changes)
            {
                if (this.IsTrackedChange(change) && this.IsFile(change))
                {
                    SharedFileSet set = this.FindFileSet(change.Item.ServerItem);
                    if (set != null)
                    {
                        ProcessingInstruction instruction = new ProcessingInstruction();
                        instruction.Change = change;
                        instruction.Changeset = changeset;
                        instruction.FileSet = set;
                        changesetProcessingInstructionList.Add(instruction);
                    }
                }
            }

            return changesetProcessingInstructionList;
        }

        private bool IsFile(Change change)
        {
            return change.Item.ItemType == ItemType.File;
        }

        private bool IsTrackedChange(Change change)
        {
            return change.ChangeType == ChangeType.Edit || change.ChangeType == ChangeType.Merge;
        }

        private void WaitForChangeset(int lastChangesetId)
        {
            while (true)
            {
                int newChangesetId = this.GetLatestChangesetId();
                if (lastChangesetId != newChangesetId) return;

                int sleepTime = (int)Settings.Default.ChangesetPollInterval.TotalMilliseconds;
                Thread.Sleep(sleepTime);
            }
        }

        public void Start()
        {
            if (this.m_WatcherThread != null)
            {
                throw new InvalidOperationException(
                    "Cannot start the watcher when it is already running."
                    );
            }

            this.m_WatcherThread = new Thread(this.Watcher);
            this.m_WatcherThread.IsBackground = true;
            this.m_WatcherThread.Start();
        }

        public void Stop()
        {
            if (this.m_WatcherThread == null)
            {
                throw new InvalidOperationException(
                    "Cannot stop the watcher when it is not running."
                    );
            }

            this.m_WatcherThread.Abort();
            this.m_WatcherThread = null;
        }
    }
}
